Skip to content

Add EqualUnordered assertion for structs with unordered slices#1851

Closed
veeceey wants to merge 2 commits intostretchr:masterfrom
veeceey:feat/issue-806-equal-unordered-slices
Closed

Add EqualUnordered assertion for structs with unordered slices#1851
veeceey wants to merge 2 commits intostretchr:masterfrom
veeceey:feat/issue-806-equal-unordered-slices

Conversation

@veeceey
Copy link

@veeceey veeceey commented Feb 17, 2026

Closes #806

I've been hitting this exact pain point for a while — when you have a struct with 15+ fields and one of them is a slice where order doesn't matter, you end up writing field-by-field assertions just to use ElementsMatch on that one slice. As @lordzsolt said in the issue, "if I have to write this all out by hand, why am I even using an assertion library?"

This adds EqualUnordered (and NotEqualUnordered) which does a deep equality comparison but treats all slices/arrays as unordered collections. It recursively walks structs, maps, pointers, and nested slices — so it handles deeply nested cases too.

type Response struct {
    Names []string
    Count int
}

expected := Response{Names: []string{"Joe", "Rick"}, Count: 2}
actual := Response{Names: []string{"Rick", "Joe"}, Count: 2}

assert.EqualUnordered(t, expected, actual) // passes
assert.Equal(t, expected, actual)          // would fail

Key details:

  • Duplicates are handled correctly: [1, 1, 2] != [1, 2, 2]
  • nil slice vs empty slice are treated as different (consistent with Equal)
  • Works with nested structs, maps with slice values, pointer types, slices of slices
  • Generated files (assertion_format.go, assertion_forward.go, require/) updated via go generate
  • All existing tests still pass

Tested locally with go test ./... — everything green.

Add deep equality comparison that treats all slices and arrays as
unordered collections. Unlike ElementsMatch which only works on
top-level slices, EqualUnordered handles slices nested within
structs, maps, pointers, and other slices.

Duplicate elements are counted correctly: [1,1,2] != [1,2,2].

Fixes stretchr#806

Signed-off-by: Varun Chawla <varun_6april@hotmail.com>
@dolmen
Copy link
Collaborator

dolmen commented Feb 17, 2026

I don't like that unordered is applied recursively.

Also, the name of the function doesn't make clear that it has this recursive behavior, so it would be easy to misuse.

So 👎

@dolmen dolmen added pkg-assert Change related to package testify/assert pkg-require Change related to package testify/require labels Feb 17, 2026
@veeceey
Copy link
Author

veeceey commented Feb 18, 2026

Thanks for the feedback @dolmen! Those are valid concerns.

You're right that the recursive unordered comparison could be surprising. A couple of thoughts:

  1. Naming: I could rename it to something like EqualUnorderedDeep or DeepEqualIgnoreOrder to make the recursive behavior explicit in the name.

  2. Non-recursive alternative: Alternatively, I could change the default to only compare top-level slices unordered, and add a separate EqualUnorderedDeep for the recursive case.

Would either of those approaches address your concern? Happy to rework this based on what the maintainers prefer.

@ccoVeille
Copy link
Collaborator

I'm unsure about the need here.

When I have to deal with something like this, which is rare, I create a normalization function I call before sending things to an asserter.

It's usually a call to a slice.SortFunc with the sort I need.

https://pkg.go.dev/slices#SortFunc

I feel like everyone needs are different, so what you implemented in the method you created might not feed someone else need. Such as the fact it handles map, slice recursively

So I would decline such feature, so more about declining the issue you are trying to solve than your implementation

@veeceey
Copy link
Author

veeceey commented Feb 19, 2026

@ccoVeille Thanks for the honest feedback -- that's a fair perspective. You're right that everyone's comparison needs are different, and a generic recursive unordered comparison might not fit most use cases well. The slices.SortFunc approach you mentioned is indeed more flexible since each caller can define their own sort criteria.

If the consensus is that this doesn't belong in testify's core, I completely understand. I'll leave the PR open for a bit in case other maintainers have thoughts, but happy to close it if the general feeling is that this is better handled in userland. Thanks for taking the time to explain your reasoning.

@veeceey
Copy link
Author

veeceey commented Feb 20, 2026

Closing this out based on the maintainer feedback. Both @dolmen and @ccoVeille raised valid concerns about the recursive behavior being too opinionated for the library's scope, and the use case being better served by userland solutions like slices.SortFunc. Thanks for taking the time to review and explain the reasoning!

@veeceey veeceey closed this Feb 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg-assert Change related to package testify/assert pkg-require Change related to package testify/require

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compare structs with unsorted slices

3 participants